/*
 * Copyright 2012 McEvoy Software Ltd.
 */
package io.milton.http.webdav2;

import io.milton.resource.Resource;
import io.milton.resource.LockingCollectionResource;
import io.milton.resource.LockableResource;
import io.milton.common.Path;
import io.milton.http.Request.Method;
import io.milton.http.Response.Status;
import io.milton.http.*;
import io.milton.http.exceptions.*;
import io.milton.http.webdav.WebDavResponseHandler;
import io.milton.webdav.utils.LockUtils;

import java.io.IOException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;

/**
 * Note that this is both a new entity handler and an existing entity handler
 *
 * @author brad
 */
public class LockHandler implements ResourceHandler {

    private static final Logger log = LoggerFactory.getLogger(LockHandler.class);

    private final WebDavResponseHandler responseHandler;
    private final HandlerHelper handlerHelper;

    public LockHandler(WebDavResponseHandler responseHandler, HandlerHelper handlerHelper) {
        this.responseHandler = responseHandler;
        this.handlerHelper = handlerHelper;
        LockUtils.init();
    }

    @Override
    public void processResource(HttpManager manager, Request request, Response response, Resource r) throws NotAuthorizedException, ConflictException, BadRequestException {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    @Override
    public String[] getMethods() {
        return new String[]{Method.LOCK.code};
    }

    @Override
    public void process(HttpManager manager, Request request, Response response) throws NotAuthorizedException, BadRequestException {
        if (!handlerHelper.checkExpects(responseHandler, request, response)) {
            return;
        }

        String host = request.getHostHeader();
        String url = HttpManager.decodeUrl(request.getAbsolutePath());

        // Find a resource if it exists
        Resource r = manager.getResourceFactory().getResource(host, url);
        if (r != null) {
            log.debug("locking existing resource: {}", r.getName());
            processExistingResource(manager, request, response, r);
        } else {
            log.debug("lock target doesnt exist, attempting lock null..");
            processNonExistingResource(manager, request, response, host, url);
        }
    }

    protected void processExistingResource(HttpManager manager, Request request, Response response, Resource resource) throws NotAuthorizedException {
        if (handlerHelper.isNotCompatible(resource, request.getMethod()) || !isCompatible(resource)) {
            responseHandler.respondMethodNotImplemented(resource, response, request);
            return;
        }
        if (!handlerHelper.checkAuthorisation(manager, resource, request)) {
            responseHandler.respondUnauthorised(resource, response, request);
            return;
        }

        handlerHelper.checkExpects(responseHandler, request, response);

        LockableResource r = (LockableResource) resource;
        LockTimeout timeout = LockTimeout.parseTimeout(request);
        String ifHeader = request.getIfHeader();
        response.setContentTypeHeader(Response.XML);
        if (ifHeader == null || ifHeader.length() == 0) {
            processNewLock(manager, request, response, r, timeout);
        } else {
            processRefresh(manager, request, response, r, timeout, ifHeader);
        }
    }

    /**
     * (from the spec) 7.4 Write Locks and Null Resources
     * <p>
     * It is possible to assert a write lock on a null resource in order to lock
     * the name.
     * <p>
     * A write locked null resource, referred to as a lock-null resource, MUST
     * respond with a 404 (Not Found) or 405 (Method Not Allowed) to any
     * HTTP/1.1 or DAV methods except for PUT, MKCOL, OPTIONS, PROPFIND, LOCK,
     * and UNLOCK. A lock-null resource MUST appear as a member of its parent
     * collection. Additionally the lock-null resource MUST have defined on it
     * all mandatory DAV properties. Most of these properties, such as all the
     * get* properties, will have no value as a lock-null resource does not
     * support the GET method. Lock-Null resources MUST have defined values for
     * lockdiscovery and supportedlock properties.
     * <p>
     * Until a method such as PUT or MKCOL is successfully executed on the
     * lock-null resource the resource MUST stay in the lock-null state.
     * However, once a PUT or MKCOL is successfully executed on a lock-null
     * resource the resource ceases to be in the lock-null state.
     * <p>
     * If the resource is unlocked, for any reason, without a PUT, MKCOL, or
     * similar method having been successfully executed upon it then the
     * resource MUST return to the null state.
     *
     * @param manager
     * @param request
     * @param response
     * @param host
     * @param url
     */
    private void processNonExistingResource(HttpManager manager, Request request, Response response, String host, String url) throws NotAuthorizedException, BadRequestException {
        String name;

        Path parentPath = Path.path(url);
        name = parentPath.getName();
        parentPath = parentPath.getParent();
        url = parentPath.toString();

        Resource r = manager.getResourceFactory().getResource(host, url);
        if (r != null) {
            if (!handlerHelper.checkAuthorisation(manager, r, request)) {
                responseHandler.respondUnauthorised(r, response, request);
                return;
            } else {
                processCreateAndLock(manager, request, response, r, name);
            }
        } else {
            log.debug("couldnt find parent to execute lock-null, returning not found");
            //respondNotFound(response,request);
            response.setStatus(Status.SC_CONFLICT);

        }
    }

    private void processCreateAndLock(HttpManager manager, Request request, Response response, Resource parentResource, String name) throws NotAuthorizedException {
        if (parentResource instanceof LockingCollectionResource) {
            log.debug("parent supports lock-null. doing createAndLock");
            LockingCollectionResource lockingParent = (LockingCollectionResource) parentResource;
            LockTimeout timeout = LockTimeout.parseTimeout(request);
            response.setContentTypeHeader(Response.XML);

            LockInfo lockInfo;
            try {
                lockInfo = LockInfoSaxHandler.parseLockInfo(request);
            } catch (SAXException | IOException ex) {
                throw new RuntimeException("Exception reading request body", ex);
            }

            // TODO: this should be refactored to return a LockResult as for existing entities

            log.debug("Creating lock on unmapped resource: " + name);
            LockToken tok = lockingParent.createAndLock(name, timeout, lockInfo);
            if (tok == null) {
                throw new RuntimeException("createAndLock returned null, from resource of type: " + lockingParent.getClass().getCanonicalName());
            }
            response.setStatus(Status.SC_CREATED);
            response.setLockTokenHeader("<opaquelocktoken:" + tok.tokenId + ">");  // spec says to set response header. See 8.10.1
            LockUtils.respondLocked(tok, request, response);

        } else {
            log.debug("parent does not support lock-null, respondong method not allowed");
            responseHandler.respondMethodNotImplemented(parentResource, response, request);
        }
    }

    @Override
    public boolean isCompatible(Resource handler) {
        return handler instanceof LockableResource;
    }

    protected void processNewLock(HttpManager milton, Request request, Response response, LockableResource r, LockTimeout timeout) throws NotAuthorizedException {
        LockInfo lockInfo;
        try {
            lockInfo = LockInfoSaxHandler.parseLockInfo(request);
        } catch (SAXException | IOException ex) {
            throw new RuntimeException("Exception reading request body", ex);
        }

        if (handlerHelper.isLockedOut(request, r)) {
            this.responseHandler.respondLocked(request, response, r);
            return;
        }

        log.debug("locking: {}", r.getName());
        LockResult result;
        try {
            result = r.lock(timeout, lockInfo);
        } catch (PreConditionFailedException ex) {
            responseHandler.respondPreconditionFailed(request, response, r);
            return;
        } catch (LockedException ex) {
            responseHandler.respondLocked(request, response, r);
            return;
        }

        if (result.isSuccessful()) {
            LockToken tok = result.getLockToken();
            log.debug("..locked ok: {}", tok.tokenId);
            response.setLockTokenHeader("<opaquelocktoken:" + tok.tokenId + ">");  // spec says to set response header. See 8.10.1
            LockUtils.respondLocked(tok, request, response);
        } else {
            LockUtils.respondLockFailure(result, request, response);
        }
    }

    protected void processRefresh(HttpManager milton, Request request, Response response, LockableResource r, LockTimeout timeout, String ifHeader) throws NotAuthorizedException {
        String token = LockUtils.parse(ifHeader);
        log.debug("refreshing lock: {}", token);
        LockResult result;
        try {
            result = r.refreshLock(token, timeout);
        } catch (PreConditionFailedException ex) {
            responseHandler.respondPreconditionFailed(request, response, r);
            return;
        }
        if (result == null) {
            throw new NullPointerException("Null lock result returned from: " + r.getClass());
        }
        if (result.isSuccessful()) {
            LockToken tok = result.getLockToken();
            response.setLockTokenHeader("<opaquelocktoken:" + tok.tokenId + ">");  // spec says to set response header. See 8.10.1
            LockUtils.respondLocked(tok, request, response);
        } else {
            LockUtils.respondLockFailure(result, request, response);
        }
    }

}
